/**
 * 
 */
package com.google.code.rookery;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.code.rookery.converter.InstanceConverter;

/**
 * @author Thom Hehl
 */
public class FactoryBuilder
    implements java.lang.reflect.InvocationHandler{

  //create a synchronized map to hold all of the builders
  private static Map<String, FactoryBuilder>       builderMap = Collections.synchronizedMap(new HashMap<String, FactoryBuilder>());

  /**
   * For a given interface, implement the factory methods using the TvoProp
   * annotations
   * @param clazz the factory interface being implemented
   * @return an instance of the factory object
   * @throws IllegalAccessException 
   * @throws InstantiationException 
   */
  public static Object createFactory(Class<?> clazz) throws InstantiationException, IllegalAccessException{
    //get the classes name
    String className = clazz.getName();
    //see if we've already created a builder for it
    FactoryBuilder builder = builderMap.get(className);
    //if not
    if(builder == null){
      //create the new one and put it in the map for later
      builderMap.put(className, builder = new FactoryBuilder(clazz));
    }
    Object factory = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, builder);
    return factory;
  }

  /**
   * For the provided class, extract all method with a FactoryMethod annotation
   * @param clazz the class being examined
   * @return an array of factory methods
   */
  public static Method[] findFactoryMethods(Class<?> clazz){
    List<Method> methods = new ArrayList<Method>();

    //loop through the methods on the factory class
    for(Method method:clazz.getMethods()){
      //check the annotations
      for(Annotation annotation:method.getAnnotations()){
        //see if it's a factory method
        if (annotation instanceof FactoryMethod) {
          //found one, add it to the list
          methods.add(method);
          //go to the next method
          break;
        }
      }
    }

    Method[] ret = new Method[methods.size()];
    ret = methods.toArray(ret);
    return ret;
  }

  private   Map<String, FactoryMethodMapping> methodMappingsMap   = new HashMap<String, FactoryMethodMapping>();
  private   Map<String, FactoryTypeMapping>   typeMappingsMap     = new HashMap<String, FactoryTypeMapping>();

  /**
   * Construct a factory implementation for a particular factory interface
   * @param clazz the factory interface
   * @throws IllegalAccessException 
   * @throws InstantiationException 
   */
  protected FactoryBuilder(Class<?> clazz) throws InstantiationException, IllegalAccessException{
    for(Method method:findFactoryMethods(clazz)){
      FactoryMethodMapping mapping = new FactoryMethodMapping(method);
      String key = mapping.getKey();
      methodMappingsMap.put(key, mapping);
      for(Class<?> parmType:method.getParameterTypes()){
        for(FactoryTypeMapping typeMapping:FactoryTypeMapping.findTypeMappings(parmType)){
          typeMappingsMap.put(parmType.getName(), typeMapping);
        }
      }
    }
  }

  /* (non-Javadoc)
   * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Class<?> returnType = method.getReturnType();
    Object ret = returnType.newInstance();

    //look for method mappings
    String key = FactoryMethodMapping.getKey(method);
    FactoryMethodMapping mapping = methodMappingsMap.get(key);
    if(mapping == null){
      throw new IllegalStateException("Unable to find mapping for method:" + method);
    }
    for(Object from:args){
      if(from!=null){
        //look for type mappings
        FactoryTypeMapping typeMapping = typeMappingsMap.get(from.getClass().getName());
        //see if we found one
        if(typeMapping!=null){
          for(InstanceConverter converter:typeMapping.getConverters()){
            Object converted = converter.convert(from);
            from = converted;
          }
          typeMapping.setValue(from, ret);
        }
        mapping.copyValues(from, ret);
      }
    }
    return ret;
  }

  /* (non-Javadoc)
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    return "FactoryBuilder [methodMappingsMap=" + methodMappingsMap + ", typeMappingsMap=" + typeMappingsMap + "]";
  }
}
